package net.moonbiter.ebs.protocols.httpjava.client;

import java.io.IOException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.apache.log4j.Logger;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtField;
import javassist.CtMethod;
import javassist.Modifier;
import javassist.NotFoundException;
import net.moonbiter.BugException;
import net.moonbiter.OperationFailureException;
import net.moonbiter.ebs.ServiceDef;
import net.moonbiter.ebs.ServiceProviderDef;
import net.moonbiter.ebs.ServicesRegister;

public class SourceGenerator {
	private static final Logger logger = Logger
			.getLogger(SourceGenerator.class);
	private ClassPool pool;

	private final static String SERVICES_FACTORY_CLASS_NAME = "ServicesFactory";

	public SourceGenerator() {
		this(ClassPool.getDefault());
	}

	public SourceGenerator(ClassPool pool) {
		this.pool = pool;
	}

	public void appendClassPathEntry(String path) throws NotFoundException {
		pool.appendClassPath(path);
	}

	public void insertClassPathEntry(String path) throws NotFoundException {
		pool.insertClassPath(path);
	}

	protected String generateMethodSignature(Class<?> returnType,
			String simpleClassName, Map<String, Class<?>> inputsTypes,
			Class<?>[] declaredExceptions) {
		StringBuffer buf = new StringBuffer();
		buf.append(returnType.getCanonicalName() + " ");
		logger.debug("added return type (buffer:" + buf.toString() + ")");
		buf.append(simpleClassName + "(");
		logger.debug("added service name (buffer:" + buf.toString() + ")");
		boolean firstParam = true;
		for (String paramName : inputsTypes.keySet()) {
			if (firstParam) {
				firstParam = false;
			} else {
				buf.append(", ");
			}
			Class<?> paramType = inputsTypes.get(paramName);
			logger.debug("param type: " + paramType);
			String paramDecl = paramType.getCanonicalName() + " " + paramName;
			logger.debug("declaring param: " + paramDecl);
			buf.append(paramDecl);
		}
		buf.append(")");
		if (declaredExceptions.length > 0) {
			buf.append(" throws ");
			boolean firstEx = true;
			for (Class<?> excep : declaredExceptions) {
				if (firstEx) {
					firstEx = false;
				} else {
					buf.append(", ");
				}
				buf.append(excep.getCanonicalName());
			}
		}
		String result = buf.toString();
		return result;
	}

	private String generateInterfaceMethod(Class<?> returnType,
			String simpleClassName, Map<String, Class<?>> inputsTypes,
			Class<?>[] declaredExceptions) {
		StringBuffer buf = new StringBuffer();
		buf.append("public ");
		buf.append(generateMethodSignature(returnType, simpleClassName,
				inputsTypes, declaredExceptions));
		buf.append(";");
		String result = buf.toString();
		logger.debug("result produced:" + result);
		return result;
	}

	private static String NL = System.getProperty("line.separator");

	private String generateClassMethod(String providerName, String serviceName,
			Class<?> returnType, String simpleClassName,
			Map<String, Class<?>> inputsTypes, Class<?>[] declaredExceptions) {
		StringBuffer buf = new StringBuffer();
		buf.append("public ");
		buf.append(generateMethodSignature(returnType, simpleClassName,
				inputsTypes, declaredExceptions));
		buf.append("{");

		// preparation of local variable map
		// of parameters values
		buf
				.append("java.util.Map/*<String,Object>*/ params = new java.util.HashMap/*<String,Object>*/();"
						+ NL);
		for (String paramName : inputsTypes.keySet()) {
			Class<?> inputType = inputsTypes.get(paramName);
			if (inputType.isPrimitive()) {
				buf.append("params.put(\"" + paramName + "\","
						+ boxingSource(inputType, paramName) + ");" + NL);
			} else {
				buf.append("params.put(\"" + paramName + "\"," + paramName
						+ ");" + NL);
			}
		}

		// invokation of service
		buf.append("Object result = null;" + NL);
		buf.append("try {" + NL);
		buf.append("\tresult = invoker.invoke(\"" + providerName + "\", \""
				+ serviceName + "\", params);" + NL);
		buf.append("} catch (" + IOException.class.getCanonicalName() + " e) {"
				+ NL);
		buf.append("\tthrow new "
				+ ServiceInvokationFailureException.class.getCanonicalName()
				+ "(\"" + providerName + "\", \"" + serviceName + "\", e);"
				+ NL);
		buf.append("} catch ("
				+ IncorrectServerResponseException.class.getCanonicalName()
				+ " e) {" + NL);
		buf.append("\tthrow new "
				+ ServiceInvokationFailureException.class.getCanonicalName()
				+ "(\"" + providerName + "\", \"" + serviceName + "\", e);"
				+ NL);
		buf.append("} catch ("
				+ ClassNotFoundException.class.getCanonicalName() + " e) {"
				+ NL);
		buf.append("\tthrow new "
				+ ServiceInvokationFailureException.class.getCanonicalName()
				+ "(\"" + providerName + "\", \"" + serviceName + "\", e);"
				+ NL);
		buf.append("} catch ("
				+ OperationFailureException.class.getCanonicalName() + " e) {"
				+ NL);
		buf.append("\tThrowable wrappedEx = e.getCause();" + NL);
		for (Class<?> aDeclaredExcep : declaredExceptions) {
			String excepName = aDeclaredExcep.getCanonicalName();
			buf.append("\tif (wrappedEx.getException instanceof " + excepName
					+ "){" + NL);
			buf.append("\t\t" + excepName + " castedEx = (" + excepName
					+ ") wrappedEx;" + NL);
			buf.append("\t\tthrow castedEx;");
			buf.append("\t}" + NL);
		}
		buf.append("\tif (wrappedEx instanceof RuntimeException){" + NL);
		buf.append("\t\tRuntimeException re = (RuntimeException)wrappedEx;"
				+ NL);
		buf.append("\t\tthrow re;" + NL);
		buf.append("\t}" + NL);
		buf.append("}" + NL);

		// casting result
		if (returnType.equals(void.class)) {
			// do not return a value
		} else if (returnType.isPrimitive()) {
			String wrapperTypeName = getWrapperTypeName(returnType);
			String conversionMethodCall = returnType.getSimpleName()+"Value()";
			buf.append("return ((" + wrapperTypeName + ") result)."+conversionMethodCall+";" + NL);
		} else {
			buf.append("return (" + returnType.getCanonicalName() + ") result;"
					+ NL);
		}

		buf.append("}");
		String result = buf.toString();
		logger.debug("result produced:" + result);
		return result;
	}

	private static String boxingSource(Class<?> type, String paramName) {
		return getWrapperTypeName(type) + ".valueOf(" + paramName + ")";
	}

	private String generateFactoryMethod(String providerName,
			CtClass providerInterface, CtClass providerClass) {
		StringBuffer buf = new StringBuffer();
		buf.append("public " + providerInterface.getName() + " get"
				+ providerName + "(){");
		buf.append("\treturn new " + providerClass.getName()
				+ "(this.serverUrl);}");
		String result = buf.toString();
		logger.debug("result produced:" + result);
		return result;
	}

	private static String getWrapperTypeName(Class<?> primitiveClass) {
		if (!primitiveClass.isPrimitive()) {
			throw new IllegalArgumentException();
		}
		Map<Class<?>, Class<?>> wrappingTypes = new HashMap<Class<?>, Class<?>>();
		wrappingTypes.put(byte.class, Byte.class);
		wrappingTypes.put(int.class, Integer.class);
		wrappingTypes.put(long.class, Long.class);
		wrappingTypes.put(char.class, Character.class);
		wrappingTypes.put(float.class, Float.class);
		wrappingTypes.put(double.class, Double.class);
		wrappingTypes.put(boolean.class, Boolean.class);
		if (wrappingTypes.containsKey(primitiveClass)) {
			return wrappingTypes.get(primitiveClass).getCanonicalName();
		} else {
			throw new BugException("primitive type not mapped: "
					+ primitiveClass.getCanonicalName());
		}
	}

	public CtClass generateInterface(String packageName, String providerName,
			ServiceProviderDef provider) {
		String canonicalName = (packageName.equals("") ? providerName
				: packageName + "." + providerName);
		CtClass provInterface = pool.makeInterface(canonicalName);
		//provInterface.setModifiers(Modifier.PUBLIC);

		for (String servName : provider.getServicesNames()) {
			ServiceDef serv = provider.getService(servName);

			CtMethod servMethod;
			try {
				servMethod = CtMethod.make(generateInterfaceMethod(serv
						.getReturnType(), servName, serv.getInputTypes(), serv
						.getDeclaredExceptions()), provInterface);
				provInterface.addMethod(servMethod);
			} catch (CannotCompileException e) {
				throw new BugException("I generated bad code", e);
			}
		}

		return provInterface;
	}

	public CtClass generateClass(String packageName, String providerName,
			ServiceProviderDef provider, CtClass interfaze) {
		String canonicalName = (packageName.equals("") ? providerName
				: packageName + "." + providerName + "Invoker");
		CtClass provClassServ = pool.makeClass(canonicalName);
		provClassServ.setModifiers(Modifier.PUBLIC);
		provClassServ.setInterfaces(new CtClass[] { interfaze });

		// fields
		CtClass stringCC = null;
		CtClass serviceInvokerCC = null;
		try {
			stringCC = pool.get(String.class.getCanonicalName());
			serviceInvokerCC = pool
					.get(ServiceInvoker.class.getCanonicalName());
		} catch (NotFoundException e) {
			throw new BugException("Existing classes not found", e);
		}

		try {
			CtField serverUrlField = new CtField(stringCC, "serverUrl",
					provClassServ);
			serverUrlField.setModifiers(Modifier.PRIVATE);
			provClassServ.addField(serverUrlField);

			CtField invokerField = new CtField(serviceInvokerCC, "invoker",
					provClassServ);
			invokerField.setModifiers(Modifier.PRIVATE);
			provClassServ.addField(invokerField);
		} catch (CannotCompileException e) {
			throw new BugException("I generated bad code", e);
		}

		// constructor
		CtConstructor constructor = new CtConstructor(
				new CtClass[] { stringCC }, provClassServ);
		constructor.setModifiers(Modifier.PUBLIC);
		try {
			constructor.setBody("{ this.serverUrl = $1; this.invoker = new "
					+ ServiceInvoker.class.getCanonicalName()
					+ "(this.serverUrl); }");
			provClassServ.addConstructor(constructor);
		} catch (CannotCompileException e) {
			throw new BugException("I generated bad code", e);
		}

		// methods
		for (String servName : provider.getServicesNames()) {
			ServiceDef serv = provider.getService(servName);

			CtMethod servMethod;
			try {
				servMethod = CtMethod
						.make(
								generateClassMethod(providerName, servName,
										serv.getReturnType(), servName, serv
												.getInputTypes(), serv
												.getDeclaredExceptions()),
								provClassServ);
				provClassServ.addMethod(servMethod);
			} catch (CannotCompileException e) {
				throw new BugException("I generated bad code", e);
			}
		}

		return provClassServ;
	}

	public CtClass generateFactory(String packageName,
			ServicesRegister register,
			Map<String, CtClass> providersInterfaces,
			Map<String, CtClass> providersClasses) {
		String canonicalName = (packageName.equals("") ? SERVICES_FACTORY_CLASS_NAME
				: packageName + "." + SERVICES_FACTORY_CLASS_NAME);
		CtClass factoryClass = pool.makeClass(canonicalName);

		CtClass stringCC = null;
		try {
			stringCC = pool.get(String.class.getCanonicalName());
		} catch (NotFoundException e) {
			throw new BugException("String class not found", e);
		}

		try {
			// adding a field of type String called serverUrl
			CtField serverUrlField = new CtField(stringCC, "serverUrl",
					factoryClass);
			serverUrlField.setModifiers(Modifier.PRIVATE);
			factoryClass.addField(serverUrlField);

			// creating a constructor taking the serverUrl
			CtConstructor constructor = new CtConstructor(
					new CtClass[] { stringCC }, factoryClass);
			constructor.setModifiers(Modifier.PUBLIC);
			constructor.setBody("{ this.serverUrl = $1; }");
			factoryClass.addConstructor(constructor);

			for (String providerName : register.getProvidersNames()) {
				ServiceProviderDef provider = register
						.getProvider(providerName);

				CtMethod provInstantiationMethod;
				try {
					if (!providersClasses.containsKey(providerName)) {
						throw new IllegalArgumentException(
								"no provider class for " + providerName);
					}
					provInstantiationMethod = CtMethod.make(
							generateFactoryMethod(providerName,
									providersInterfaces.get(providerName),
									providersClasses.get(providerName)),
							factoryClass);
					factoryClass.addMethod(provInstantiationMethod);
				} catch (CannotCompileException e) {
					throw new BugException("I generated bad code", e);
				}
			}

		} catch (CannotCompileException e) {
			throw new BugException("I generated bad code", e);
		}

		return factoryClass;
	}

	public List<CtClass> generateAll(String packageName,
			ServicesRegister register) {
		List<CtClass> generated = new LinkedList<CtClass>();

		// interfaces
		Map<String, CtClass> interfaces = new HashMap<String, CtClass>();
		Map<String, CtClass> classes = new HashMap<String, CtClass>();
		for (String provName : register.getProvidersNames()) {
			ServiceProviderDef prov = register.getProvider(provName);
			CtClass interfaze = generateInterface(packageName, provName, prov);
			generated.add(interfaze);
			CtClass clazz = generateClass(packageName, provName, prov,
					interfaze);
			generated.add(clazz);
			interfaces.put(provName, interfaze);
			classes.put(provName, clazz);
		}

		generated.add(generateFactory(packageName, register, interfaces, classes));

		return generated;
	}

	private static String paramNameFromClassName(CtClass parameter) {
		String parameterName = parameter.getName();
		parameterName = parameterName
				.substring(parameterName.lastIndexOf(".") + 1);
		if (parameterName.indexOf('[') != -1) {
			parameterName = parameterName.substring(0, parameterName
					.indexOf('['));
		}
		String firstCharLowerCase = parameterName.substring(0, 1).toLowerCase();
		parameterName = firstCharLowerCase.concat(parameterName.substring(1));
		return parameterName;
	}
}
